Clinical Data Journey(Phase 1): Raw Demographics to SDTM DM using R

Objective

In this article, we’ll convert a raw demographics dataset into an SDTM DM domain using R, following CDISC SDTM standards.

Raw Data

The raw dataset contains study-specific variables collected from the EDC(Electronic Data Capture) system. Before submission, these variables must be standardized into SDTM.

Load Required Packages

library(tidyverse)   # Data manipulation
library(lubridate)   # Date handling

Import the data

raw_dm <- read_csv(
  "RAW_Demo.csv",
  show_col_types = FALSE
)

Review the Raw Dataset

head(raw_dm)
# A tibble: 6 × 15
  STUDYID   SITEID  Patient_ID   Age Gender RACE   ETHNICITY HEIGHT_CM WEIGHT_KG
  <chr>     <chr>   <chr>      <dbl> <chr>  <chr>  <chr>         <dbl>     <dbl>
1 ONCO-2026 SITE004 SUBJ0001      62 Male   WHITE  NOT HISP…      167.      78.2
2 ONCO-2026 SITE014 SUBJ0002      84 Female BLACK… NOT HISP…      164.     100. 
3 ONCO-2026 SITE010 SUBJ0003      71 Female WHITE  NOT HISP…      168       79.6
4 ONCO-2026 SITE007 SUBJ0004      49 Female ASIAN  NOT HISP…      174       79.8
5 ONCO-2026 SITE002 SUBJ0005      45 Female ASIAN  NOT HISP…      154.      73.2
6 ONCO-2026 SITE002 SUBJ0006      64 Female BLACK… NOT HISP…      145       92.4
# ℹ 6 more variables: BMI <dbl>, BIRTH_DATE <chr>, COUNTRY <chr>,
#   RAND_DATE <chr>, ARM <chr>, TREATMENT_START_DATE <chr>

Initial Data Assessment

Variable names
names(raw_dm)
 [1] "STUDYID"              "SITEID"               "Patient_ID"          
 [4] "Age"                  "Gender"               "RACE"                
 [7] "ETHNICITY"            "HEIGHT_CM"            "WEIGHT_KG"           
[10] "BMI"                  "BIRTH_DATE"           "COUNTRY"             
[13] "RAND_DATE"            "ARM"                  "TREATMENT_START_DATE"
Number of subjects
n_distinct(raw_dm$Patient_ID)
[1] 500
Check duplicate subjects
raw_dm %>%
count(Patient_ID) %>%
filter(n > 1)
# A tibble: 0 × 2
# ℹ 2 variables: Patient_ID <chr>, n <int>
Missing values
colSums(is.na(raw_dm))
             STUDYID               SITEID           Patient_ID 
                   0                    0                    0 
                 Age               Gender                 RACE 
                   0                    0                    0 
           ETHNICITY            HEIGHT_CM            WEIGHT_KG 
                   0                    7                   11 
                 BMI           BIRTH_DATE              COUNTRY 
                  18                    0                    0 
           RAND_DATE                  ARM TREATMENT_START_DATE 
                   0                    0                    0 

Missing values were observed in HEIGHT_CM, WEIGHT_KG, and BMI. These variables are not used in the SDTM DM domain, so they do not affect the DM transformation.

Raw-to-SDTM Variable Mapping

Raw Variable SDTM DM Variable Transformation
STUDYID STUDYID Direct mapping
Patient_ID SUBJID Rename
STUDYID + SITEID + Patient_ID USUBJID Derive unique subject identifier
Gender SEX Convert Male/Female → M/F
ETHNICITY ETHNIC Rename
BIRTH_DATE BRTHDTC Convert to ISO 8601
TREATMENT_START_DATE RFXSTDTC Convert to ISO 8601
ARM ARM Direct mapping
ARM ARMCD Create coded value
SITEID SITEID Direct mapping
COUNTRY COUNTRY Direct mapping

Note:

  • CDISC SDTM requires controlled terminology.Raw values such as “Male” and “Female” are standardized to “M” and “F”.

  • Dates were converted to the ISO 8601 format (YYYY-MM-DD), which is required by CDISC SDTM.

SDTM Transformation Approach:

The raw demographics data is transformed into the SDTM DM structure by deriving standard variables, applying CDISC controlled terminology, converting dates into ISO 8601 format, and creating unique subject identifiers required for regulatory submission.

Transform Raw Data into SDTM DM

DM <- raw_dm %>%

  mutate(

    DOMAIN = "DM",

    USUBJID = paste(
      STUDYID,
      SITEID,
      Patient_ID,
      sep = "-"
    ),

    SUBJID = Patient_ID,

    SEX = case_when(
      Gender == "Male" ~ "M",
      Gender == "Female" ~ "F",
      TRUE ~ NA_character_
    ),

    AGE = Age,
    ETHNIC = ETHNICITY,

    ARMCD = case_when(
      ARM == "ARM A" ~ "A",
      ARM == "ARM B" ~ "B",
      TRUE ~ NA_character_
    ),

    BRTHDTC =
      format(
        dmy(BIRTH_DATE),
        "%Y-%m-%d"
      ),

    RFXSTDTC =
      format(
        dmy(TREATMENT_START_DATE),
        "%Y-%m-%d"
      )

  )

Select SDTM Standard Variables

DM <- DM %>%

select(

STUDYID,
DOMAIN,
USUBJID,
SUBJID,
SITEID,

AGE,
SEX,
RACE,
ETHNIC,

BRTHDTC,
COUNTRY,

ARMCD,
ARM,

RFXSTDTC

)

Review SDTM DM

glimpse(DM)
Rows: 500
Columns: 14
$ STUDYID  <chr> "ONCO-2026", "ONCO-2026", "ONCO-2026", "ONCO-2026", "ONCO-202…
$ DOMAIN   <chr> "DM", "DM", "DM", "DM", "DM", "DM", "DM", "DM", "DM", "DM", "…
$ USUBJID  <chr> "ONCO-2026-SITE004-SUBJ0001", "ONCO-2026-SITE014-SUBJ0002", "…
$ SUBJID   <chr> "SUBJ0001", "SUBJ0002", "SUBJ0003", "SUBJ0004", "SUBJ0005", "…
$ SITEID   <chr> "SITE004", "SITE014", "SITE010", "SITE007", "SITE002", "SITE0…
$ AGE      <dbl> 62, 84, 71, 49, 45, 64, 39, 83, 74, 51, 34, 76, 56, 75, 35, 4…
$ SEX      <chr> "M", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "F", "…
$ RACE     <chr> "WHITE", "BLACK OR AFRICAN AMERICAN", "WHITE", "ASIAN", "ASIA…
$ ETHNIC   <chr> "NOT HISPANIC OR LATINO", "NOT HISPANIC OR LATINO", "NOT HISP…
$ BRTHDTC  <chr> "1961-02-22", "1939-11-19", "1953-01-03", "1974-08-28", "1978…
$ COUNTRY  <chr> "GBR", "BEL", "AUS", "JPN", "USA", "USA", "USA", "ESP", "JPN"…
$ ARMCD    <chr> "B", "A", "B", "B", "A", "B", "A", "A", "B", "A", "A", "B", "…
$ ARM      <chr> "ARM B", "ARM A", "ARM B", "ARM B", "ARM A", "ARM B", "ARM A"…
$ RFXSTDTC <chr> "2024-04-22", "2024-11-08", "2024-05-31", "2024-02-10", "2024…

Validate the SDTM DM Dataset

Check 1: Duplicate Subjects
anyDuplicated(DM$USUBJID)
[1] 0
Check 2: One Record per Subject
DM %>%
count(USUBJID) %>%
filter(n > 1)
# A tibble: 0 × 2
# ℹ 2 variables: USUBJID <chr>, n <int>
Check 3: Missing Required Variables
DM %>%
summarise(
missing_STUDYID = sum(is.na(STUDYID)),
missing_USUBJID = sum(is.na(USUBJID)),
missing_SEX = sum(is.na(SEX))
)
# A tibble: 1 × 3
  missing_STUDYID missing_USUBJID missing_SEX
            <int>           <int>       <int>
1               0               0           0
Check 4: Required variable presence
required_vars <- c(
  "STUDYID",
  "DOMAIN",
  "USUBJID",
  "SUBJID"
)

required_vars %in% names(DM)
[1] TRUE TRUE TRUE TRUE

Export SDTM Dataset

write_csv(
  DM,
  "SDTM_DM.csv"
)

Key Learning

Clinical programming involves much more than renaming variables. It requires transforming raw clinical data into standardized SDTM datasets by applying CDISC standards, controlled terminology, derivations, and validation checks.

During Raw → SDTM transformation, we:

  • Map source variables to CDISC SDTM structures
  • Standardize variable names
  • Convert dates into ISO 8601 format
  • Apply controlled terminology
  • Derive unique subject identifiers (USUBJID)
  • Perform quality checks
  • Prepare analysis-ready regulatory datasets

Conclusion

In this article, we transformed a raw demographics dataset into an SDTM DM domain using R. We reviewed the source data, mapped variables to SDTM standards, standardized dates and controlled terminology, validated the resulting dataset, and exported the final SDTM DM dataset. This workflow provides a practical foundation for understanding SDTM development in R before moving on to metadata-driven programming and additional SDTM domains.

In the next article, we’ll explore metadata-assisted SDTM programming using a metadata specification and sdtm.oak. We’ll see how metadata can improve consistency, traceability, and maintainability in SDTM development before moving towards formal metadata frameworks such as metacore.